Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

// TODO

要播放动画,不管怎样,我们都要先拿到模型,Gltf模型其实是一种Asset,因此我们在第三章中讨论的如何加载资产的那一整套,都可以完美的平移过来。

首先我们需要一个Resource来存储句柄,以便在不同的系统之间传递。当然也可以传递一些别的东西,不过目前而言我们主要关心的是其中的Handle

#![allow(unused)]
fn main() {
#[derive(Resource)]
struct Fox(Handle<Gltf>);
}

然后,在startup阶段我们的setup系统中,只需要直接加载即可。

#![allow(unused)]
fn main() {
fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.insert_resource(Fox(asset_server.load(FOX_PATH)));
    //....
}
}

在这里,我们将用上我们在第2章中介绍的条件运行(参见2.3.2 run_if)。我们希望在资源加载完成并且配置完成之前,不要运行我们的动画系统。(或者,我们也可以采用state的方法来切换系统的运行,你能想到怎么做吗?)

#![allow(unused)]
fn main() {
// 存储动画相关的资源
#[derive(Resource)]
struct Animations {
    // 至于AnimationNodeIndex和AnimationGraph是什么,等待下文我们揭晓
    animations: Vec<AnimationNodeIndex>,
    graph_handle: Handle<AnimationGraph>,
}

// 这个系统在资产加载完成之前将会一直尝试运行
app.add_systems(
    Update,
    spawn_fox_asset_when_ready.run_if(not(resource_exists::<Animations>)),
)// 这个系统只有在加载完成后运行
.add_systems(
    Update,
    keyboard_control.run_if(resource_exists::<Animations>),
)
}

spawn_fox_asset_when_ready系统中,我们依然采用我们的三板斧。使用is_loaded_with_dependencies来判断是否已经加载完成(详见3.1.1节,要时常复习才能温故知新!)

#![allow(unused)]
fn main() {
fn spawn_fox_asset_when_ready(
    mut commands: Commands,
    fox_handle: Res<Fox>,
    asset_server: Res<AssetServer>,
    gltfs: Res<Assets<Gltf>>,
    mut graphs: ResMut<Assets<AnimationGraph>>,
) {
    // 如果尚未加载完成,那么先不执行
    if !asset_server.is_loaded_with_dependencies(&fox_handle.0) {
        return;
    }
	// 然后后利用句柄来获得我们的资产,你还记得吗?
    let fox = gltfs
        .get(&fox_handle.0)
        .expect("a loaded asset should exist in the glTF assets collection");

    // 很快你就会知道这里的三个动画对应了什么了
    // 现在,只需要知道我们有了一个AnimationGraph类型的graph
    // 还有了一个Vec<NodeIndex>类型的node_indices
    let (graph, node_indices) = AnimationGraph::from_clips([
        fox.named_animations["Run"].clone(),
        fox.named_animations["Walk"].clone(),
        fox.named_animations["Survey"].clone(),
    ]);

    // 保存这些动画信息,以便我们后续使用
    let graph_handle = graphs.add(graph);
    commands.insert_resource(Animations {
        animations: node_indices,
        graph_handle,
    });

    // 现在,我们才向bevy中添加真正的模型
    commands
        .spawn(SceneRoot(
            fox.default_scene
                .clone()
                .expect("a default scene exists in this file"),
        ))
    	// 这里用到了observe,还记得observe有什么用吗?(详见2.7.1节,再次强调温故知新的重要性~)
        .observe(setup_scene);
}

// 现在,每当SceneInstanceReady触发时,该函数将会被执行
// 可是SceneInstanceReady是什么?他的完整路径是bevy::scene::SceneInstanceReady
// 简而言之,这是bevy的一个内置事件,其定义如下,每当整个场景加载完成之后会被触发
// pub struct SceneInstanceReady {
//     pub entity: Entity,
//     pub instance_id: InstanceId,
// }
// 而AnimationPlayer组件,是跟随scene自动添加的
// 所以当SceneInstanceReady触发的时候,AnimationPlayer也已经存在
fn setup_scene(
    _ready: On<SceneInstanceReady>,
    mut commands: Commands,
    animations: Res<Animations>,
    player: Single<(Entity, &mut AnimationPlayer)>,
) {
    // 直接将内部的两个组件的所有权拿出来
    let (entity, mut player) = player.into_inner();
    // 然后我们需要new一个AnimationTransitions
    // 如果你用过css,那么你一定也知道transition有什么用,这里的AnimationTransitions
    // 也是一样的,当动画切换的时候,将会在动画之间插入平滑的过渡
    let mut transitions = AnimationTransitions::new();
	// 立刻开始重复播放第一个动画
    transitions
        .play(&mut player, animations.animations[0], Duration::ZERO)
        .repeat();
	// 这里的commands.entity对应了我们在第2章介绍的EntityComands,还记得吗?
    // 因此我们向scene根实体上,插入了这两个新的组件
    commands
        .entity(entity)
        .insert(AnimationGraphHandle(animations.graph_handle.clone()))
        .insert(transitions);
}

}

这些代码看下来真是让人大汗淋漓。你可能心里会想,怎么那么多没见过的东西?AnimationNodeIndex是什么?AnimationGraph是什么?AnimationTransitions又是什么?AnimationGraphHandle又是什么??

让我们来一个个翻一翻源代码看看吧。AnimationNodeIndex其实只是一个别名,其本质上不过是一个含有u32类型的枚举罢了,这个数字标识了一个动画在gltf中的索引。这解释了为什么我们使用transitions.play方法时需要他。

#![allow(unused)]
fn main() {
/// The index of either an animation or blend node in the animation graph.
///
/// These indices are the way that [animation players] identify each animation.
///
/// [animation players]: crate::AnimationPlayer
pub type AnimationNodeIndex = NodeIndex<u32>;
}

AnimationGraph是一个有向无环图,感兴趣的读者可以查看文档,他其实相当复杂。描述了多个动画应该如何混合在一起。这是什么意思呢?我们在设计动画时,比如行走和攻击,一般都会设计成为两个单独的动画。如果不能将其混合起来一起播放,那么你行走的时候攻击时角色就会发生诡异的漂移(即脚不动但是角色还在跑,这在很多粗制滥造的游戏中很常见)。

观察其代码,可以发现其由graphrootmask_groups构成。AnimationGraphNode描述了如何对动画进行混合。HashMap<AnimationTargetId, u64>则描述了每块动画骨骼的uuid和掩码组之间的关系。掩码组是什么意思?

为什么这里是一个HashMap<AnimationTargetId, u64>类型呢?其实这是一种典型的位图。u64类型含有64个bit位。因此每个骨骼,最多支持定义 64 个不同的掩码组

举个例子,一个复杂的模型可能有几百根骨骼,但你可能只想给其中的一部分(比如只有右手、或者只有上半身)分配掩码组。HashMap 只存储那些被分配了组的骨骼。你在 mask_groups 里把所有属于“上半身”的骨骼 ID 都标记为 1 << 0(第 0 组)。节点 A 播放“奔跑”动画(不带掩码,作用于全身)。节点 B 播放“挥剑”或“换弹”动画,但你给这个节点设置一个掩码,指定它只作用于第 0 组。角色就可以一边跑(下半身由节点 A 控制),一边做攻击动作(上半身被节点 B 覆盖)。

这方面的混合其实非常复杂,三言两语是搞不定的。如果对动画混合更感兴趣的读者,则需要查阅更详细的资料,这里就不再赘述。

至于AnimationGraphHandle,这名字显而易见,由于AnimationGraph实际上是一种asset,因此当然也有对应的handle

#![allow(unused)]
fn main() {
pub struct AnimationGraph {
    pub graph: Graph<AnimationGraphNode, ()>,
    pub root: NodeIndex,
    pub mask_groups: HashMap<AnimationTargetId, u64>,
}

pub struct AnimationTargetId(pub Uuid);
}

AnimationTransitions是一个用于控制动画之间过度的“增强型”AnimationPlayer。前面我们说当场景生成时,AnimationPlayer会自动添加到场景的任何根动画中。要使用AnimationTransitions你还必须把AnimationGraphHandle也同时放在一个实体上。仔细一想这也是正常的。因为动画过渡,本身就是利用动画图来实现的。

#![allow(unused)]
fn main() {
// 第一次动画必须使用transitions.play而不能使用AnimationPlayer上的play方法
// 因为AnimationTransitions需要记录动画的顺序,如果直接操作AnimationPlayer开始
// 会导致AnimationTransitions 无法确定动画的切换关系,从而导致过渡效果通常不正确。
let mut transitions = AnimationTransitions::new();
transitions
    .play(&mut player, animations.animations[0], Duration::ZERO)
    .repeat();
// 并且我们把这两个组件都插入到了根上
commands
    .entity(entity)
    .insert(AnimationGraphHandle(animations.graph_handle.clone()))
    .insert(transitions);
}

最后,一切大功告成,只需要在按下不同的按键时切换不同动画即可。

#![allow(unused)]
fn main() {
fn keyboard_control(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
    animations: Res<Animations>,
    mut current_animation: Local<usize>,
) {
    // 这里的player是一个AnimationPlayer
    // 而transitions则是我们插入的AnimationTransitions
    for (mut player, mut transitions) in &mut animation_players {
        // 我们可以获取下一个动画的NodeIndex
        let Some((&playing_animation_index, _)) = player.playing_animations().next() else {
            continue;
        };
		// 当按下特定的按键时,可以控制播放器的行为,比如播放暂停
        if keyboard_input.just_pressed(KeyCode::Space) {
            let playing_animation = player.animation_mut(playing_animation_index).unwrap();
            if playing_animation.is_paused() {
                playing_animation.resume();
            } else {
                playing_animation.pause();
            }
        }
        // 还可以控制播放速度
         if keyboard_input.just_pressed(KeyCode::ArrowUp) {
            let playing_animation = player.animation_mut(playing_animation_index).unwrap();
            let speed = playing_animation.speed();
            playing_animation.set_speed(speed * 1.2);
        }
        // 对于动画的切换,我们需要计算下一个索引,并重新使用transitions来切换
        // 必须使用transitions.play()!否则过渡会出问题
        if keyboard_input.just_pressed(KeyCode::Enter) {
            *current_animation = (*current_animation + 1) % animations.animations.len();
            transitions
                .play(
                    &mut player,
                    animations.animations[*current_animation],
                    Duration::from_millis(250),
                )
                .repeat();
        }

       // ..... 等等的类似逻辑
    }
}

}